package com.kevin.mapping.script.lexer;

import com.kevin.mapping.script.exception.ParserException;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

public class Lexer {
    private final String expression;
    private final Map<String, Word> words = new HashMap<>();
    private final Stack<Integer> stack = new Stack<>();
    private int line = 1;
    private char peek = ' ';
    private int index = 0;

    public Lexer(String expression) {
        if (null == expression || "".equals(expression)) {
            throw new ParserException("expression is nil");
        }
        this.expression = expression;
    }

    public boolean readChar(char c) {
        if (readChar()) {
            return peek == c;
        }
        return false;
    }

    public boolean readChar() {
        if (index >= expression.length()) {
            peek = ' ';
            return false;
        }
        peek = expression.charAt(index++);
        return true;
    }

    public void back(int times) {
        while (times-- > 0 && !stack.empty()) {
            index = stack.pop();
        }
    }


    public Token scan() {
        if (stack.isEmpty() || stack.peek() != index) {
            stack.push(index);
        }

        Token ret = null;
        for (; ; readChar()) {
            if (peek != ' ' && peek != '\t') {
                if (peek == '\n') {
                    line = line + 1;
                } else {
                    break;
                }
            }
            if (index >= expression.length()) {
                peek = ' ';
                break;
            }
        }

        switch (peek) {
            case '\'':
            case '\"':
                readChar();
                StringBuilder sb = new StringBuilder();
                while (readChar()) {
                    if (peek == '\'' || peek == '\"') {
                        break;
                    }
                    sb.append(peek);
                }
                return new StrValue(Type.STR_VALUE, sb.toString());
            case '&':
                if (readChar('&')) ret = Word.AND;
                else ret = new Token('&');

                return ret;
            case '|':
                if (readChar('|')) ret = Word.OR;
                else ret = new Token('|');
                return ret;
            case '=':
                if (readChar('=')) ret = Word.EQUAL;
                else ret = new Token('=');
                return ret;
            case '!':
                if (readChar('=')) ret = Word.NE;
                else ret = new Token('!');
                return ret;
            case '<':
                if (readChar('=')) ret = Word.LTE;
                else ret = new Token('<');
                return ret;
            case '>':
                if (readChar('=')) ret = Word.GTE;
                else ret = new Token('>');
                return ret;

        }

        if (Character.isDigit(peek)) {
            ret = numberValue();
            return ret;
        }
        if (Character.isLetter(peek)) {
            ret = pathValue();
            return ret;
        }
        ret = new Token(peek);
        readChar();
        return ret;
    }

    private Token pathValue() {
        StringBuilder sb = new StringBuilder();
        sb.append(peek);
        while (readChar() && Character.isLetterOrDigit(peek) || peek == '[' || peek == ']' || peek == '.') {
            sb.append(peek);
        }
        Word word = Word.KEYWORD.get(sb.toString());
        if (null != word) {
            return word;
        }
        return new PathValue(Type.PATH, sb.toString());
    }

    @NotNull
    private Token numberValue() {
        StringBuilder sb = new StringBuilder();
        sb.append(peek);
        while (readChar() && Character.isDigit(peek)) {
            sb.append(peek);
        }
        if (peek == '.') {
            sb.append(peek);
        } else {
            return new IntegerValue(Type.INTEGER, Integer.valueOf(sb.toString()));
        }
        while (readChar() && Character.isDigit(peek)) {
            sb.append(peek);
        }

        if (peek != 'e' && peek != 'E') {
            return new DoubleValue(Type.DOUBLE, Double.valueOf(sb.toString()));
        }
        sb.append(peek);
        readChar();
        if (peek == '+' || peek == '-') {
            sb.append(peek);
        }
        while (readChar() && Character.isDigit(peek)) {
            sb.append(peek);
        }
        return new DoubleValue(Type.DOUBLE, Double.valueOf(sb.toString()));
    }


    public int getLine() {
        return line;
    }

    public void setLine(int line) {
        this.line = line;
    }
}
